/*! jsObservable: http://github.com/BorisMoore/jsviews */
/*
 * Subcomponent of JsViews
 * Data change events for data-linking
 *
 * Copyright 2012, Boris Moore and Brad Olenick
 * Released under the MIT License.
 */
// informal pre beta commit counter: 7

(function ( $, undefined ) {
	$.observable = function( data, options ) {
		return $.isArray( data )
			? new ArrayObservable( data )
			: new ObjectObservable( data );
	};

	var splice = [].splice;

	function ObjectObservable( data ) {
		if ( !this.data ) {
			return new ObjectObservable( data );
		}

		this._data = data;
		return this;
	};

	$.observable.Object = ObjectObservable;

	ObjectObservable.prototype = {
		_data: null,

		data: function() {
			return this._data;
		},

		afterChange: function( path, callback ) {

		},

		setProperty: function( path, value ) { // TODO in the case of multiple changes (object): raise single propertyChanges event (which may span different objects, via paths) with set of changes.
			if ( $.isArray( path ) ) {
				// This is the array format generated by serializeArray. However, this has the problem that it coerces types to string,
				// and does not provide simple support of convertTo and convertFrom functions.
				// TODO: We've discussed an "objectchange" event to capture all N property updates here. See TODO note above about propertyChanges.
				for ( var i = 0, l = path.length; i < l; i++ ) {
					var pair = path[i];
					this.setProperty( pair.name, pair.value );
				}
			} else
			if ( typeof( path ) === "object" ) {
				// Object representation where property name is path and property value is value.
				// TODO: We've discussed an "objectchange" event to capture all N property updates here. See TODO note above about propertyChanges.
				for ( var key in path ) {
					this.setProperty( key, path[ key ]);
				}
			} else {
				// Simple single property case.
				var object = this._data,
					leaf = getLeafObject( object, path );

				path = leaf[1];
				leaf = leaf[0];
				if ( leaf ) {
					this._setProperty( leaf, path, value );
				}
			}
			return this;
		},

		_setProperty: function( leaf, path, value ) {
			var setter,
			property = leaf[ path ];

			if ( $.isFunction( property ) ) {
				// Case of property setter/getter - with convention that property() is getter and property( value ) is setter
				setter = property;
				property = property.call( leaf ); //get
			}

			if ( property != value ) { // test for non-strict equality, since serializeArray, and form-based editors can map numbers to strings, etc.
				if ( setter ) {
					setter.call( leaf, value );		//set
					value = setter.call( leaf );	//get updated value
				} else {
					leaf[ path ] = value;
				}
				this._trigger( leaf, { path: path, value: value, oldValue: property } );
			}
		},

		_trigger: function( target, eventArgs ) {
			$( target ).triggerHandler( "propertyChange", eventArgs );
		}
	};

	function getLeafObject( object, path ) {
		if ( object && path ) {
			var parts = path.split(".");

			path = parts.pop();
			while ( object && parts.length ) {
				object = object[ parts.shift() ];
			}
			return [ object, path ];
		}
		return [];
	};

	function ArrayObservable( data ) {
		if ( !this.data ) {
			return new ArrayObservable( data );
		}

		this._data = data;
		return this;
	};

	function validateIndex( index ) {
		if ( typeof index !== "number" ) {
			throw "Invalid index.";
		}
	};

	$.observable.Array = ArrayObservable;

	ArrayObservable.prototype = {
		_data: null,

		data: function() {
			return this._data;
		},

		insert: function( index, data ) {
			validateIndex( index );

			if ( arguments.length > 1 ) {
				data = $.isArray( data ) ? data : [ data ];  // TODO: Clone array here?
				// data can be a single item (including a null/undefined value) or an array of items.

				if ( data.length > 0 ) {
					this._insert( index, data );
				}
			}
			return this;
		},

		_insert: function( index, data ) {
			splice.apply( this._data, [ index, 0 ].concat( data ));
			this._trigger( { change: "insert", index: index, items: data } );
		},

		remove: function( index, numToRemove ) {
			validateIndex( index );

			numToRemove = ( numToRemove === undefined || numToRemove === null ) ? 1 : numToRemove;
			if ( numToRemove && index > -1 ) {
				var items = this._data.slice( index, index + numToRemove );
				numToRemove = items.length;
				if ( numToRemove ) {
					this._remove( index, numToRemove, items );
				}
			}
			return this;
		},

		_remove: function( index, numToRemove, items ) {
			this._data.splice( index, numToRemove );
			this._trigger( { change: "remove", index: index, items: items } );
		},

		move: function( oldIndex, newIndex, numToMove ) {
			validateIndex( oldIndex );
			validateIndex( newIndex );

			numToMove = ( numToMove === undefined || numToMove === null ) ? 1 : numToMove;
			if ( numToMove ) {
				var items = this._data.slice( oldIndex, oldIndex + numToMove );
				this._move( oldIndex, newIndex, numToMove, items );
			}
			return this;
		},

		_move: function( oldIndex, newIndex, numToMove, items ) {
			this._data.splice( oldIndex, numToMove );
			this._data.splice.apply( this._data, [ newIndex, 0 ].concat( items ) );
			this._trigger( { change: "move", oldIndex: oldIndex, index: newIndex, items: items } );
		},

		refresh: function( newItems ) {
			var oldItems = this._data.slice( 0 );
			this._refresh( oldItems, newItems );
			return this;
		},

		_refresh: function( oldItems, newItems ) {
			splice.apply( this._data, [ 0, this._data.length ].concat( newItems ));
			this._trigger( { change: "refresh", oldItems: oldItems } );
		},

		_trigger: function( eventArgs ) {
			$([ this._data ]).triggerHandler( "arrayChange", eventArgs );
		}
	};
})(jQuery);
